﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using Lucene.Net.Analysis;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.QueryParsers;
using Lucene.Net.Search;
using Lucene.Net.Store;
using NLog;

namespace TfsGiggle.Core
{
    /// <summary>
    /// Model handling Lucene and IndexWriter/Reader
    /// </summary>
    public sealed class IndexModel : IDisposable
    {
        static LoggerWithEventId _logger = (LoggerWithEventId)LogManager.GetCurrentClassLogger(typeof(LoggerWithEventId));
        static IndexReader _reader;
        static IndexSearcher _searcher;
        static Analyzer _analyzer;
        static CancellationToken _cancelToken;
        readonly string _luceneDirectory;
        readonly static ScoreDocComparer _scoreDocComparer = new ScoreDocComparer();
        readonly static object _writerLock = new object();
        readonly string[] _fieldsToSearch;

        /// <summary>
        /// ctor
        /// </summary>
        /// <param name="luceneDirectory"></param>
        /// <param name="analyzer"></param>
        /// <param name="tfsConfigurationServer"></param>
        /// <param name="cancelToken"></param>
        internal IndexModel(string luceneDirectory, Analyzer analyzer, ITfsConfigurationServer tfsConfigurationServer, CancellationToken cancelToken)
        {
            _logger.Trace("luceneDirectory:{0}, tfsUri:{1}", luceneDirectory, tfsConfigurationServer.Name);
            _luceneDirectory = luceneDirectory;
            _analyzer = analyzer;
            _cancelToken = cancelToken;

            _fieldsToSearch = new string[]  {
                SourceControlDocument.FieldNames.ChangesetId,
                SourceControlDocument.FieldNames.WorkitemId,
                SourceControlDocument.FieldNames.ChangesetComment,
                SourceControlDocument.FieldNames.ChangesetCommitter,
                SourceControlDocument.FieldNames.WorkitemTitle,
                SourceControlDocument.FieldNames.Contents,
                SourceControlDocument.FieldNames.DocumentKey,
                SourceControlDocument.FieldNames.Modified,};

            try
            {
                var dir = new DirectoryInfo(luceneDirectory);
                var luceneDir = FSDirectory.Open(dir);
                if (!dir.Exists)
                {
                    _logger.Info("Creating new LuceneDirectory at {0}", luceneDirectory);
                    using (var writer = new IndexWriter(luceneDir, _analyzer, true, IndexWriter.MaxFieldLength.LIMITED))
                    {
                        ;
                    }

                }
                if (IndexWriter.IsLocked(luceneDir))
                {
                    _logger.Warn("LuceneDirectory {0} is locked, unlocking", luceneDirectory);
                    IndexWriter.Unlock(luceneDir);
                    _logger.Warn("LuceneDirectory {0} unlocked", luceneDirectory);
                }
                _reader = IndexReader.Open(luceneDir, true);
                _searcher = new IndexSearcher(_reader);
                TfsSourceControl.Instance.Open(tfsConfigurationServer);
            }
            catch (Exception ex)
            {
                _logger.ErrorExceptionWithEventId(EventId.IndexModelConstructorFailed, "ctor failed", ex);
            }
            _logger.Trace("exit");
        }

        private void Dispose(bool disposing)
        {
            _logger.Trace("enter");
            if (disposing)
            {
                if (_reader != null)
                {
                    _reader.Dispose();
                    _reader = null;
                }
                if (_searcher != null)
                {
                    _searcher.Dispose();
                    _searcher = null;
                }
            }
            _logger.Trace("exit");
        }

        private bool Exists(string teamProjectCollectionId, string path)
        {
            _logger.Trace("enter");
            var timer = new Stopwatch();
            timer.Start();
            var exists = LuceneDocumentId(teamProjectCollectionId, path) >= 0;
            timer.Stop();
            ETW.TfsGiggleProvider.EventWriteIndexExistsEvent(teamProjectCollectionId, path, timer.ElapsedTicks);
            _logger.Trace("exit");
            return exists;
        }

        /// <summary>
        /// Create a new IndexWriter
        /// </summary>
        internal IndexWriter GetIndexWriter()
        {
            _logger.Trace("enter");
            var dir = FSDirectory.Open(new DirectoryInfo(_luceneDirectory));
            if (!IndexWriter.IsLocked(dir))
            {
                _logger.Info("New IndexWriter created");
                return new IndexWriter(dir, _analyzer, false, IndexWriter.MaxFieldLength.LIMITED);
            }
            _logger.Error("LuceneDirectory is currently locked");
            return null;
        }

        /// <summary>
        /// Delete this document
        /// </summary>
        /// <param name="writer"></param>
        /// <param name="teamProjectCollectionId"></param>
        /// <param name="path"></param>
        internal void Delete(IndexWriter writer, string teamProjectCollectionId, string path)
        {
            if (writer == null) throw new ArgumentNullException("writer");
            if (string.IsNullOrWhiteSpace(teamProjectCollectionId)) throw new ArgumentNullException("teamProjectCollectionId");
            if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException("path");
            _logger.Trace("teamProjectCollectionId:{0} path:{1}", teamProjectCollectionId, path);

            if (_cancelToken.IsCancellationRequested)
            {
                _logger.Trace("Cancellation requested");
            }

            try
            {
                if (Exists(teamProjectCollectionId, path))
                {
                    var term = new Term(SourceControlDocument.FieldNames.DocumentKey, GetDocumentKey(teamProjectCollectionId, path));
                    writer.DeleteDocuments(term);
                    Statistics.Instance.NumberOfFilesDeleted.Add(1);
                    Statistics.Instance.NumberOfDocuments.Add(-1);
                    _logger.Info("document '{0}:{1}' deleted", teamProjectCollectionId, path);
                }
                else
                {
                    _logger.Trace("Document:'{0}:{1}' was not found in the index, its a new document", teamProjectCollectionId, path);
                }
            }
            catch (Exception ex)
            {
                _logger.ErrorExceptionWithEventId(EventId.DocumentDelete, "Exception thrown", ex);
                throw;
            }
            _logger.Trace("exit");
        }

        /// <summary>
        /// Create the documentkey by combining the 2 parameters
        /// </summary>
        /// <param name="teamProjectCollectionId"></param>
        /// <param name="path"></param>
        /// <returns></returns>
        internal static string GetDocumentKey(string teamProjectCollectionId, string path)
        {
            if (string.IsNullOrWhiteSpace(teamProjectCollectionId)) throw new ArgumentNullException("teamProjectCollectionId");
            if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException("path");
            return string.Format("{0}:{1}", teamProjectCollectionId, path);
        }

        /// <summary>
        /// Extract the teamProjectCollectionId from the documentkey
        /// </summary>
        /// <param name="documentKey"></param>
        /// <returns></returns>
        internal static string GetTeamProjectCollectionIdFromDocumentKey(string documentKey)
        {
            if (string.IsNullOrWhiteSpace(documentKey)) throw new ArgumentNullException("documentKey");
            var parts = documentKey.Split(':');
            if (parts == null || parts.Length != 2)
            {
                throw new ArgumentOutOfRangeException("No ':' in documentKey");
            }
            return parts[0];
        }

        /// <summary>
        /// Exract the Path from the documentkey
        /// </summary>
        /// <param name="documentKey"></param>
        /// <returns></returns>
        internal static string GetPathFromDocumentKey(string documentKey)
        {
            if (string.IsNullOrWhiteSpace(documentKey)) throw new ArgumentNullException("documentKey");
            var parts = documentKey.Split(':');
            if (parts == null || parts.Length != 2)
            {
                throw new ArgumentOutOfRangeException("No ':' in documentKey");
            }
            return parts[1];
        }

        /// <summary>
        /// True if we can process this file based on its extension
        /// </summary>
        /// <param name="fileName"></param>
        /// <returns></returns>
        internal bool IsSupportedFileExtension(string fileName)
        {
            var extension = Path.GetExtension(fileName);
            return Settings.RegexForKnownFileExtensions.IsMatch(extension);
        }

        /// <summary>
        /// Add this document with changeset and workitem information
        /// </summary>
        /// <param name="writer"></param>
        /// <param name="teamProjectCollectionId"></param>
        /// <param name="path"></param>
        /// <returns></returns>
        internal bool Add(IndexWriter writer, string teamProjectCollectionId, string path)
        {
            if (writer == null) throw new ArgumentNullException("writer");
            if (string.IsNullOrWhiteSpace(teamProjectCollectionId)) throw new ArgumentNullException("teamProjectCollectionId");
            if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException("path");

            _logger.Trace("teamProjectCollectionId:{0} path:{1}", teamProjectCollectionId, path);
            var timer = new Stopwatch();
            timer.Start();
            if (_cancelToken.IsCancellationRequested)
            {
                _logger.Trace("Cancellation requested");
                return false;
            }

            try
            {
                var vcs = TfsSourceControl.Instance.GetVersionControlServer(teamProjectCollectionId);

                // Incoming changesets are a point in time, future changesets may have deleted the file
                if (!vcs.ServerItemExists(path, Microsoft.TeamFoundation.VersionControl.Client.ItemType.File))
                {
                    _logger.Info("The item {0} was not found, it may have been deleted", path);
                    return false;
                }
                var item = vcs.GetItem(path);
                if (item == null)
                {
                    return false;
                }

                var documentId = LuceneDocumentId(teamProjectCollectionId, path);
                if (documentId >= 0)
                {
                    _logger.Trace("Document:{0}:{1} was found with id:{2}", teamProjectCollectionId, path, documentId);

                    // Determine if we need to update this document, i.e. delete it first
                    var document = _searcher.Doc(documentId);
                    var changesets = document.GetValues(SourceControlDocument.FieldNames.ChangesetId);
                    if (changesets != null && changesets.Length > 0)
                    {
                        if (!changesets.Contains(item.ChangesetId.ToString()))
                        {
                            _logger.Trace("Item:{0} not found with changeset-id {1}, deleting prior to adding (an update)", path, item.ChangesetId);
                            Delete(writer, teamProjectCollectionId, path);
                        }
                        else
                        {
                            _logger.Trace("item '{0}' was not updated, not adding it to the index", path);
                            return false;
                        }
                    }
                }
                var newDocument = SourceControlDocument.Document(item, teamProjectCollectionId, path);
                writer.AddDocument(newDocument);
                Statistics.Instance.NumberOfFilesAdded.Add(1);
                Statistics.Instance.NumberOfDocuments.Add(1);
                timer.Stop();
                ETW.TfsGiggleProvider.EventWriteIndexAddEvent(teamProjectCollectionId, path, timer.ElapsedTicks);
                _logger.Trace("exit. item '{0}' was added", path);
                return true;
            }
            catch (Exception ex)
            {
                _logger.ErrorExceptionWithEventId(EventId.DocumentAdd, "Exception caught", ex);
                throw;
            }
        }

        /// <summary>
        /// Get the Lucene document id
        /// </summary>
        /// <param name="teamProjectCollectionId"></param>
        /// <param name="path"></param>
        /// <returns></returns>
        internal int LuceneDocumentId(string teamProjectCollectionId, string path)
        {
            if (string.IsNullOrWhiteSpace(teamProjectCollectionId)) throw new ArgumentNullException("teamProjectCollectionId");
            if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException("path");
            _logger.Trace("path:{0}:{1}", teamProjectCollectionId, path);

            if (_cancelToken.IsCancellationRequested)
            {
                _logger.Trace("Cancellation requested");
            }

            int luceneDocumentId = -1;
            var term = new Term(SourceControlDocument.FieldNames.DocumentKey, GetDocumentKey(teamProjectCollectionId, path));
            var termDocs = _reader.TermDocs(term);
            while (termDocs.Next())
            {
                luceneDocumentId = termDocs.Doc();
                _logger.Trace("DocumentId for '{0}' is {1}", path, luceneDocumentId);
                break;
            }
            _logger.Trace("exit");
            return luceneDocumentId;
        }

        /// <summary>
        /// Commit each changeset
        /// </summary>
        /// <param name="writer"></param>
        internal void Commit(IndexWriter writer)
        {
            if (writer == null) throw new ArgumentNullException("writer");
            _logger.Trace("enter");

            // Only one changeset should be processing at a time, but just to be sure...
            lock (_writerLock)
            {
                writer.Close(true);
                var newReader = _reader.Reopen();
                if (newReader != _reader)
                {
                    _logger.Info("_reader is not current, creating a new reader and searcher...");
                    _searcher.Close();
                    _reader.Close();
                    _reader = newReader;
                    _searcher = new IndexSearcher(_reader);
                    _logger.Info("Done");
                }
            }
            _logger.Trace("exit");
        }

        /// <summary>
        /// Executes the query and returns <seealso cref="Settings.MaxNumberOfDocumentsToReturn"/> documents found
        /// </summary>
        /// <param name="userQuery"></param>
        /// <returns></returns>
        public QueryResultDTO Query(string userQuery)
        {
            _logger.Trace("userQuery:'{0}'", userQuery);
            var queryResultDTO = new QueryResultDTO();

            if (string.IsNullOrWhiteSpace(userQuery))
            {
                queryResultDTO.ErrorMessage = "userQuery is null or whitespace";
                return queryResultDTO;
            }
            userQuery = userQuery.Trim();
            if (!Settings.RegexForValidatingUserQuery.IsMatch(userQuery))
            {
                var msg = string.Format("The userQuery '{0}' did not pass the validation pattern '{1}'", userQuery, Settings.RegexForValidatingUserQuery.ToString());
                _logger.Trace(msg);
                queryResultDTO.ErrorMessage = msg;
                return queryResultDTO;
            }

            var timer = new Stopwatch();
            timer.Start();
            try
            {
                var parser = new MultiFieldQueryParser(Lucene.Net.Util.Version.LUCENE_29, _fieldsToSearch, _analyzer);
                var query = parser.Parse(userQuery);
                var collector = TopScoreDocCollector.create(Settings.MaxNumberOfDocumentsToReturn, false);
                _searcher.Search(query, collector);
                Statistics.Instance.NumberOfSearchesPerformed.Add(1);
                Statistics.Instance.SessionMostRecentSearches.Add(userQuery);
                var scoreDocs = collector.TopDocs().ScoreDocs;
                if (scoreDocs == null || scoreDocs.Length == 0)
                {
                    _logger.Trace("No results were found for the query:'{0}'", userQuery);
                }
                queryResultDTO = QueryResult.GetQueryResultDTO(from p in scoreDocs select _searcher.Doc(p.doc));
            }
            catch (ParseException ex)
            {
                queryResultDTO.ErrorMessage = ex.ToString();
                queryResultDTO.Error = ErrorType.LuceneParseException;
            }
            catch (Exception ex)
            {
                queryResultDTO.ErrorMessage = ex.ToString();
                queryResultDTO.Error = ErrorType.UnknownException;
            }
            timer.Stop();
            ETW.TfsGiggleProvider.EventWriteSearchQueryEvent(userQuery, timer.ElapsedTicks);
            Statistics.Instance.TotalSearchTimeInTicks.Add(timer.ElapsedTicks);
            queryResultDTO.TotalNumberOfDocumentsInTheIndex = NumberOfDocuments;
            _logger.Trace("exit");
            return queryResultDTO;
        }

        /// <summary>
        /// Gets the number of documents in the index
        /// </summary>
        public int NumberOfDocuments
        {
            get { return _reader.NumDocs(); }
        }

        /// <summary>
        /// Return this document
        /// </summary>
        /// <param name="documentId"></param>
        /// <returns></returns>
        public QueryResultDTO GetDocument(int documentId)
        {
            _logger.Trace("enter");
            if (documentId < _reader.NumDocs() && documentId >= 0)
            {
                if (!_reader.IsDeleted(documentId))
                {
                    var document = _searcher.Doc(documentId);
                    if (document != null)
                    {
                        _logger.Trace("exit");
                        return QueryResult.GetQueryResultDTO(new Document[] { document });
                    }
                }
                else
                {
                    _logger.Trace("The document:{0} has been deleted", documentId);
                }
            }
            _logger.Trace("exit");
            return null;
        }

        /// <summary>
        /// Go away
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}